--[[
Advanced Portals (advanced_portals), A minetest add-on that allows certain users to create and place portals on their minetest world.

Copyright (c) 2020 Genshin <emperor_genshin@hotmail.com>
License: GPLv3
--]]


minetest.register_privilege("portals_admin", {"Grants the ability to configure any portal owned by players", give_to_singleplayer = false})
minetest.register_privilege("portals", {"Grants the ability to place and configure their own portals", give_to_singleplayer = false})

local storage = minetest.get_mod_storage()
local footer = "Version 1.0 - By: Genshin"

local from_node_metadata = {}
local previous_input_data = {} --Previous Input Data stored to be reused during active portal placement on the map via on_construct
local saved_input_data = {} --Previous Input Data stored to be reused when active portal is being right_clicked
local portal_list_changes = {}
local previous_portal_ids = {}

local debug = minetest.settings:get_bool("enable_portals_debug") or false

portal_list_changes = {}
--[Local Function] Convert CSV (String list with commas) to a Table Array
local function convert_csv_to_table_array(s)
  local result = {}
  s = s .. ','
  local t = {}
  local fieldstart = 1
  repeat

    if string.find(s, '^"', fieldstart) then
      local a, c
      local i  = fieldstart
      repeat

        a, i, c = string.find(s, '"("?)', i+1)
      until c ~= '"'
      if not i then return result end
      local f = string.sub(s, fieldstart+1, i-1)
      table.insert(t, (string.gsub(f, '""', '"')))
      fieldstart = string.find(s, ',', i) + 1
    else
      local nexti = string.find(s, ',', fieldstart)
      table.insert(t, string.sub(s, fieldstart, nexti-1))
      fieldstart = nexti + 1
    end
  until fieldstart > string.len(s)
  for k,v in pairs(t) do
    result[#result+1] = string.gsub(v, "%s+", "")
  end

  return result
end


--[Local Function] Convert CSV (Comma separated values) to XYZ Coordinates
local function convert_csv_to_xyz(s)
  local result = {}
  local cancel = false
  s = s .. ','
  local t = {}
  local fieldstart = 1

  repeat

    if string.find(s, '^"', fieldstart) then
      local a, c
      local i  = fieldstart
      repeat

        a, i, c = string.find(s, '"("?)', i+1)
      until c ~= '"'
      if not i then return result end
      local f = string.sub(s, fieldstart+1, i-1)
      table.insert(t, (string.gsub(f, '""', '"')))
      fieldstart = string.find(s, ',', i) + 1
    else
      local nexti = string.find(s, ',', fieldstart)
      table.insert(t, string.sub(s, fieldstart, nexti-1))
      fieldstart = nexti + 1
    end
  until fieldstart > string.len(s)

  --Convert Table into Coordinates
  for k,v in pairs(t) do
    local number = k
    local value = string.gsub(v, "%s+", "")

    if string.match(v, "%d+") then

      if number == 1 then
        result["x"] = tonumber(value)
      elseif number == 2 then
        result["y"] = tonumber(value)
      elseif number == 3 then
        result["z"] = tonumber(value)
      elseif number > 3 then
        break
      end

    else

      cancel = true

    end
  end

  if cancel == true then
    return {}
  end

  return result
end


--[Local Function] Show Portal Configuration Menu
local function show_portal_configuration_menu(name, meta)

  if minetest.check_player_privs(name, {portals_admin=true}) or minetest.check_player_privs(name, {portals=true}) then
    local owner = meta:get_string("owner")

    --Debug Stuff to see and verify if local owner is being called
    if debug == true then
      minetest.chat_send_player(name, "<[DEBUG]: Owner = "..tostring(owner)..">")
      print("<[DEBUG]: Owner = "..tostring(owner)..">")
    end

    if name ~= owner or (name ~= owner and minetest.check_player_privs(name, {portals_admin=false})) then
      minetest.chat_send_player(name, "You are not authorized to configure this portal!")
      goto end_portal_config

    elseif name == owner or (name ~= owner and minetest.check_player_privs(name, {portals_admin=true})) then
      goto show_portals_form
    end

    ::show_portals_form::

    --Load Previous Inputs
    local status = meta:get_string("status")
    local facedir = meta:get_string("facedir")
    local portal_name = meta:get_string("portal_name")
    local portal_nametag = meta:get_string("nametag")
    local node = meta:get_string("portal_node_name")
    local linked_portal_name = meta:get_string("linked_portal")
    local portal_pos = minetest.deserialize(meta:get_string("portal_pos"))
    local private = meta:get_string("private")
    local linked = meta:get_string("linked")
    local coordinates = meta:get_string("coordinates")
    local whitelist = meta:get_string("whitelist")
    local private_int = nil
    local linked_int = nil
    local status_color = nil
    local formspec = nil

    from_node_metadata[name] = {
     ["nametag"] = portal_nametag,
     ["owner"] = owner,
     ["status"] = status,
     ["facedir"] = facedir,
     ["portal_name"] = portal_name,
     ["portal_node_name"] = node,
     ["portal_pos"] = portal_pos,
     ["node"] = node,
     ["linked_portal_name"] = linked_portal_name,
     ["private"] = private,
     ["linked"] = linked,
     ["coordinates"] = coordinates,
     ["whitelist"] = whitelist 
    }

    --Debug Stuff to see and verify if local metadata strings are being stored in table "from_node_metadata" correctly.
    if debug == true then
      minetest.chat_send_player(name, "<[DEBUG]: "..tostring(minetest.serialize(from_node_metadata[name]))..">")
      print("<[DEBUG]: "..tostring(minetest.serialize(from_node_metadata[name]))..">")
    end

    if private == "Yes" then
      private_int = 1
    elseif private == "No" then
      private_int = 2
    end

    if linked == "Yes" then
      linked_int = 1
    elseif linked == "No" then
      linked_int = 2
    end

    if status == "Inactive" then
      status_color = "#e8484a"
    elseif status == "Active" then
      status_color = "#00f03c"
    end

    if portal_name == "" then

     formspec = {
      "size[8,8]",
      "background[-0.30,-0.34;8.5,8.5;portals_gui_bg.png;true]",

      "image[0,-0.85;3.1,3.5;portals_gui_logo.png]",

      "label[3,0.10;Portal Configuration Menu]",
      "label[3,0.55;"..minetest.colorize("#cacaca", "Status: ")..minetest.colorize(status_color, status).."]",
      "label[3,0.80;"..minetest.colorize("#cacaca", "Owner: ")..minetest.colorize("#fff0a2", owner).."]",

      "image[6.60,2.90;0.70,2.43;portals_gui_linked.png]",

      "field[1.95,1.90;5.25,0.8;portal_name;"..minetest.colorize("#ff0004", "* ").."Portal ID;"..portal_name.."]",
      "field[1.95,2.90;5.25,0.8;set_coords;"..minetest.colorize("#ff0004", "* ").."Destination (If not Linked);"..minetest.formspec_escape(coordinates).."]",

      "field[1.95,3.90;4.25,0.8;whitelist; Who else can use this portal? (If Private);"..whitelist.."]",
      "label[5.70,3.23;"..minetest.colorize("#ffe44b", "Private?").."]",
      "dropdown[5.70,3.59;1,1;private;Yes,No;"..private_int.."]",

      "field[1.95,4.90;4.25,0.8;linked_portal;"..minetest.colorize("#ff0004", "* ").."Linked Portal (If Linked);"..linked_portal_name.."]",
      "label[5.70,4.22;"..minetest.colorize("#ffe44b", "Linked?").."]",
      "dropdown[5.70,4.58;1,1;linked;Yes,No;"..linked_int.."]",

      "field[1.95,5.90;4.25,0.8;portal_nametag; Portal Tag;"..portal_nametag.."]",

      "image_button_exit[2.5,6.70;1.5,0.8;portals_gui_button.png;apply;Set]",
      "image_button_exit[4.0,6.70;1.5,0.8;portals_gui_button.png;cancel;Cancel]",
      "label[0,7.90;"..minetest.colorize("#acacac", minetest.formspec_escape(footer)).."]"
     }

    else

     formspec = {
      "size[8,8]",
      "background[-0.30,-0.34;8.5,8.5;portals_gui_bg.png;true]",

      "image[0,-0.85;3.1,3.5;portals_gui_logo.png]",

      "label[3,0.10;Portal Configuration Menu]",
      "label[3,0.55;"..minetest.colorize("#cacaca", "Status: ")..minetest.colorize(status_color, status).."]",
      "label[3,0.80;"..minetest.colorize("#cacaca", "Owner: ")..minetest.colorize("#fff0a2", owner).."]",

      "image[6.60,2.90;0.70,2.43;portals_gui_linked.png]",

      "label[1.80,1.50;Portal ID]",
      "label[1.85,1.80;"..minetest.formspec_escape(portal_name).."]",
      "field[1.95,2.90;5.25,0.8;set_coords;"..minetest.colorize("#ff0004", "* ").."Destination (If not Linked);"..minetest.formspec_escape(coordinates).."]",

      "field[1.95,3.90;4.25,0.8;whitelist; Who else can use this portal? (If Private);"..whitelist.."]",
      "label[5.70,3.23;"..minetest.colorize("#ffe44b", "Private?").."]",
      "dropdown[5.70,3.59;1,1;private;Yes,No;"..private_int.."]",

      "field[1.95,4.90;4.25,0.8;linked_portal;"..minetest.colorize("#ff0004", "* ").."Linked Portal (If Linked);"..linked_portal_name.."]",
      "label[5.70,4.22;"..minetest.colorize("#ffe44b", "Linked?").."]",
      "dropdown[5.70,4.58;1,1;linked;Yes,No;"..linked_int.."]",

      "field[1.95,5.90;4.25,0.8;portal_nametag; Portal Tag;"..portal_nametag.."]",

      "image_button_exit[2.5,6.70;1.5,0.8;portals_gui_button.png;apply;Set]",
      "image_button_exit[4.0,6.70;1.5,0.8;portals_gui_button.png;cancel;Cancel]",
      "label[0,7.90;"..minetest.colorize("#acacac", minetest.formspec_escape(footer)).."]"
     }


    end

    minetest.show_formspec(name, "portals:configuration_menu", table.concat(formspec, ""))
  end

  ::end_portal_config::
  return
end


--[Function] Get portal data
function get_portal_data(portal)

if not portal then
return nil
end

return minetest.deserialize(storage:get_string(portal))
end


--[Function] Get portal data
function get_portal_data(portal)

if not portal then
return nil
end

return minetest.deserialize(storage:get_string(portal))
end


--[Function] Get portal particle values - TODO
function get_portal_particle_values(portal, valuetype)
local bool = storage:get_string(portal.."_activation_particles") or false
local texture = storage:get_string(portal.."_activation_particle_texture")
local interval = storage:get_string(portal.."_activation_particles_interval")
local radius = storage:get_int(portal.."_activation_particle_spread")

if valuetype == "flag" then
return bool
elseif valuetype == "texture" then
return texture
elseif valuetype == "delay" then
return interval
elseif valuetype == "spread" then
return radius
end

if not portal or not texture or not interval or not radius then
return nil
end

return minetest.deserialize(storage:get_string(portal))
end



function get_portal_handler_origin_height(portal, class)

if class == "warp_handler" then
return storage:get_int(portal.."_warp_handler_height")
elseif class == "nametag" then
return storage:get_int(portal.."_nametag_height")
end

return nil
end


--[Function] Get Portal Handler Sound by type
function get_portal_handler_sounds(portal, soundtype)
local sounds_list = storage:get_string(portal.."_sounds")
local distance = storage:get_int(portal.."_sound_distance")

if sounds_list then
sounds_list = minetest.deserialize(sounds_list)

if soundtype == "portal_activate" then

return sounds_list.activate
elseif soundtype == "portal_ambiance" then

return sounds_list.portal_ambiance
elseif soundtype == "portal_warp" then

return sounds_list.portal_warp
elseif soundtype == "sound_distance" then

return distance
else
return nil
end

return nil
end

return nil
end


--[Function] Register Portal Node
function register_portal(name, def)

--Temporary Data
local groups_data = {}
local tmp = {}

--generate a separate groups table for active portals
if def.groups then
for k,v in pairs(def.groups) do
local group = k
local group_value = v
groups_data[group] = group_value
end
end

local active_portal_groups = groups_data

--Clear temporary data
groups_data = {}

--for active portal nodes, make sure they are hidden away from the inventory so they can be swapped silently, also make sure no one can place it either
active_portal_groups["not_in_creative_inventory"] = 1

--Inactive Portal
minetest.register_node("advanced_portals:"..name.."_inactive",{

  description = def.description.." (Inactive)",
  drawtype = "mesh",
  mesh = def.mesh,
  inventory_image = def.inventory_image or nil,
  light_source = def.light_source or nil,
  tiles = {def.tiles[1], "portal_deactivated.png"},
  use_texture_alpha = true,
  groups = def.groups,
  paramtype2 = "facedir",
  selection_box = def.selection_box,
  drop = "advanced_portals:"..name.."_inactive",
  sounds = def.sounds or nil,
  collision_box = def.collision_box,
  on_rotate = screwdriver.rotate_simple,

  after_place_node = function(pos, placer, itemstack, pointed_thing)

    local pname = placer:get_player_name()

    if minetest.check_player_privs(pname, {portals_admin = true}) or minetest.check_player_privs(pname, {portals = true}) then

      local meta = minetest.get_meta(pos)
      local node = minetest.get_node(pos)

      meta:set_string("portal_name", tostring(""))
      meta:set_string("owner", tostring(pname)) --The owner of the portal
      meta:set_string("portal_node_name", name) --The node name of the portal
      meta:set_string("facedir", node.param2)
      meta:set_string("status", "Inactive")
      meta:set_string("facedir", node.param2) --The face direction of the portal, it's needed for swapping nodes
      meta:set_string("owner", tostring(pname)) --The owner of the portal
      meta:set_string("status", "Inactive") --[Flag] Is the portal active or not?
      meta:set_string("portal_pos", minetest.serialize(pos)) --The exact location of the portal
      meta:set_string("private", "No") --[Flag] is this portal private accessible?
      meta:set_string("linked", "No") ----[Flag] is this portal linked to another portal?

    else

      minetest.chat_send_player(pname, "**You are not authorized to place portals on the map!")
      minetest.remove_node(pos)
      return itemstack
    end

  end,

  on_rightclick = function(pos, node, player, itemstack, pointed_thing)

    local pname = player:get_player_name()
    local meta = minetest.get_meta(pos)
    local owner = meta:get_string("owner")

    show_portal_configuration_menu(pname, meta)

    return itemstack
  end

  
})

--Active Portal
minetest.register_node("advanced_portals:"..name.."_active",{

  description = def.description.." (Inactive)\n "..minetest.colorize("#ff0004","*You shouldn't have this!"),
  drawtype = "mesh",
  mesh = def.mesh,
  is_portal = true,
  inventory_image = def.inventory_image or nil,
  light_source = 30,
  tiles = {
    def.tiles[1],
    {
      name = def.activation_sprite,
      animation = {
        type = "vertical_frames",
        aspect_w = 16,
        aspect_h = 16,
        length = 0.3,
      },
      backface_culling = true,
    },
  },
	
  use_texture_alpha = true,
  groups = active_portal_groups,
  paramtype2 = "facedir",
  selection_box = def.selection_box,
  drop = "advanced_portals:"..name.."_inactive",
  sounds = def.sounds or nil,
  collision_box = def.collision_box,
  on_rotate = screwdriver.rotate_simple,

  on_construct = function(pos)
  
      local meta = minetest.get_meta(pos)
      local old_data = previous_input_data

    if old_data == nil then else

    meta:set_string("portal_name", old_data.portal_name)
    meta:set_string("portal_node_name", old_data.portal_node_name)
    meta:set_string("facedir", old_data.facedir)
    meta:set_string("owner", old_data.owner)
    meta:set_string("portal_pos", minetest.serialize(old_data.portal_pos))
    meta:set_string("private", old_data.private)
    meta:set_string("linked", old_data.linked)
    meta:set_string("linked_portal", old_data.linked_portal)
    meta:set_string("coordinates", old_data.coordinates)
    meta:set_string("whitelist", old_data.whitelist)
    meta:set_string("status", old_data.status)

      --Debug Stuff to see and verify if local owner is being called
      if debug == true then
        minetest.chat_send_player(old_data.owner, "<[DEBUG]: Active portal successfully set old metadata at on_construct >")
        print("<[DEBUG]: Active portal successfully set old metadata at on_construct >")
      end

    previous_input_data = nil

    end

  end,

  on_place = function(itemstack, placer, pointed_thing)
    local pname = placer:get_player_name()

    minetest.log("warning", pname.." attempt to place a illegal node on the map. kicking user "..pname.." from the server due to possible exploitation attempts.")

    minetest.chat_send_all(minetest.colorize("#ff8c00", "[EXPLOITATION PREVENTION]:"..pname.." attempted to place unobtainable blocks on the map. kicking user "..pname.." from the server due to possible exploitation attempts."))

    minetest.kick_player(pname, "[Exploitation Attempts] Attempt to place unobtainable illegal blocks on the map. This incident will be reported to the server\'s host!")

    return
  end,

  --Clear Data
  on_destruct = function(pos)
    local meta = minetest.get_meta(pos)
    local portal_name = meta:get_string("portal_name")
    local portals_list = storage:get_string("portals_list")

    storage:set_string(portal_name, "")

    if not (portals_list == nil or portals_list == "") then

      portals_list = minetest.deserialize(portals_list)
      portal_list_changes = portals_list


      if portals_list[portal_name] then
      portal_list_changes[portal_name] = nil

      storage:set_string("portals_list", minetest.serialize(portal_list_changes))
      end

      portal_list_changes = {}

    end

  end,

  on_rightclick = function(pos, node, player, itemstack, pointed_thing)

    local pname = player:get_player_name()
    local meta = minetest.get_meta(pos)
    local owner = meta:get_string("owner")
    local portal_name = meta:get_string("portal_name")
    local old_data = saved_input_data[portal_name]

    if old_data == nil then else

    meta:set_string("nametag", old_data.nametag)
    meta:set_string("portal_name", old_data.portal_name)
    meta:set_string("portal_node_name", old_data.portal_node_name)
    meta:set_string("facedir", old_data.facedir)
    meta:set_string("owner", old_data.owner)
    meta:set_string("portal_pos", minetest.serialize(old_data.portal_pos))
    meta:set_string("private", old_data.private)
    meta:set_string("linked", old_data.linked)
    meta:set_string("linked_portal", old_data.linked_portal)
    meta:set_string("coordinates", old_data.coordinates)
    meta:set_string("whitelist", old_data.whitelist)
    meta:set_string("status", old_data.status)

    saved_input_data[portal_name] = nil

      --Debug Stuff to see and verify if local owner is being called
      if debug == true then
        minetest.chat_send_player(pname, "<[DEBUG]: Active portal successfully set information changes to its metadata via on_rightckick >")
        print("<[DEBUG]: Active portal successfully set information changes to its metadata via on_rightckick >")
      end

    end

    show_portal_configuration_menu(pname, meta)

    return itemstack
  end
  
})

  --Set dynamic intergers
  if minetest.registered_nodes["advanced_portals:"..name.."_active"] then
    storage:set_int(name.."_warp_handler_height", def.warp_handler_height)
    storage:set_int(name.."_nametag_height", def.nametag_height)
    storage:set_int(name.."_teleport_radius", def.teleport_radius)
    storage:set_int(name.."_warp_height", def.warp_height)
    storage:set_string(name.."_sounds", minetest.serialize(def.portal_sounds))
    storage:set_int(name.."_sound_distance", def.portal_sound_distance)
    storage:set_string(name.."_activation_particles", tostring(def.activation_particles))
    storage:set_string(name.."_activation_particle_texture", def.activation_particle_texture)
    storage:set_int(name.."_activation_particle_interval", def.activation_particle_interval)
    storage:set_int(name.."_activation_particle_spread", def.activation_particle_spread)
    return
  end

end



--[Fields Handler] Portal Configuration Menu Handler
minetest.register_on_player_receive_fields(function(player, formname, fields)
  if formname == "portals:configuration_menu" then

--[Local Function] Update Portal Config
  if fields.apply then
  goto portal_config_fields
  else
  return
  end

  ::portal_config_fields::

  local pname = player:get_player_name()
  local from_meta = from_node_metadata[pname]
  local portal_name = from_meta.portal_name

  --Debug Stuff to see and verify if portal name is being called
  if debug == true then
  minetest.chat_send_all("1) "..tostring(fields.portal_name)..", "..tostring(portal_name))
  end

  if fields.portal_name and portal_name == "" then

  portal_name = fields.portal_name

    --Debug Stuff to see and verify if portal name is being called
  if debug == true then
  minetest.chat_send_all("2) "..tostring(fields.portal_name)..", "..tostring(portal_name))
  end

  elseif not fields.portal_name and portal_name ~= "" then

  fields.portal_name = portal_name

  --Debug Stuff to see and verify if portal name is being called
  if debug == true then
  minetest.chat_send_all("3) "..tostring(fields.portal_name)..", "..tostring(portal_name))
  end

  end

  if (fields.portal_name == nil or fields.portal_name == "") and portal_name == "" then

  --Debug Stuff to see and verify if portal name is being called
  if debug == true then
  minetest.chat_send_all(tostring(fields.portal_name)..", "..tostring(portal_name))
  end

  minetest.chat_send_player(pname, "**You must give a ID to this portal!")
  return
  else


  local dir = from_meta.facedir
  local node = tostring(from_meta.portal_node_name)
  local owner = tostring(from_meta.owner)
  local status = tostring(from_meta.status)
  local portal_pos = from_meta.portal_pos
  local private_int = nil
  local linked_int = nil
  local is_private = nil
  local is_linked = nil
  local coordinates = {x = 0, y = 0, z = 0}
  local handler_ent = nil
  local nametag_ent = nil
  local handler = nil
  local nametag = nil
  local portal_data = storage:get_string(fields.portal_name)
  local portals_list = storage:get_string("portals_list")

    --Debug Stuff to see and verify if local metadata strings exists in table "from_node_metadata".
    if debug == true then
      minetest.chat_send_all("<[DEBUG]: "..tostring(minetest.serialize(from_meta))..">")
      print("<[DEBUG]: "..tostring(minetest.serialize(from_meta))..">")
    end

  if portal_data == nil or portal_data == "" then else
  portal_data = minetest.deserialize(portal_data)
  end


  if portals_list == nil or portals_list == "" then
    storage:set_string("portals_list", minetest.serialize(portal_list_changes))

      --Debug Stuff to see and verify if portal list changes are present
    if debug == true then
    minetest.chat_send_player(pname, "<[DEBUG]: "..tostring(minetest.serialize(portal_list_changes))..">")
    end
    

  else

    

    portals_list = minetest.deserialize(storage:get_string("portals_list"))
    portal_list_changes = portals_list

      --Debug Stuff to see and verify if portal list changes are present
    if debug == true then
    minetest.chat_send_player(pname, "<[DEBUG]: "..tostring(minetest.serialize(portal_list_changes))..">")
    end 

  end  


  --update previous portal name and clear it's old data if name does not match with the old portal name
  if portal_name == nil or portal_name == "" then
  
  elseif portal_name ~= fields.portal_name then
  storage:set_string(portal_name, "")

  

  --clear old portal name from the list
  if portals_list[portal_name] then

  
  portal_list_changes[portal_name] = nil
  previous_portal_ids[portal_name] = fields.portal_name
  storage:set_string("previous_portal_ids", minetest.serialize(previous_portal_ids))
  end

  end

  --Check if portal ID exists on the list, if it does, thenn cancel activation.
  if not portals_list[fields.portal_name] then
  
  portal_list_changes[fields.portal_name] = true

  elseif portals_list[fields.portal_name] and portal_name == fields.portal_name then

  elseif portals_list[fields.portal_name] then
  minetest.chat_send_player(pname, "**Unable to change Portal\'s ID, Portal ID \""..fields.portal_name.."\" already exist!")
  return
  end

  if fields.private == "Yes" then
  private_int = 1
  is_private = true
  elseif fields.private == "No" then
  private_int = 2
  is_private = false
  end

  if fields.linked == "Yes" then
  linked_int = 1
  is_linked = true
  elseif fields.linked == "No" then
  linked_int = 2
  is_linked = false
  end

  if is_linked == false then
  coordinates = convert_csv_to_xyz(fields.set_coords)

  if next(coordinates) == nil then
  minetest.chat_send_player(pname, "**You must enter proper XYZ coordinates for the destination!")
  return
  end
  
  elseif is_linked == true then
  local linked_portal_data = storage:get_string(fields.linked_portal)

      --Debug Stuff to see and verify if linked portal data from mod storage has successfully loaded
      if debug == true then
        minetest.chat_send_player(pname, "<[DEBUG]: Linked Portal Data = "..tostring(linked_portal_data).." >")
        print("<[DEBUG]: Linked Portal Data = "..tostring(linked_portal_data).." >")
      end


  if linked_portal_data == nil or linked_portal_data == "" then
  minetest.chat_send_player(pname, "**You must type a existing portal in order to link this portal with another one!")
  return
  else

  linked_portal_data = minetest.deserialize(linked_portal_data)

  local target_portal = linked_portal_data["portal_name"]
  if target_portal ~= fields.linked_portal then
  minetest.chat_send_player(pname, "**You must type a existing portal in order to link this portal with another one!")
  return
  elseif target_portal == fields.portal_name then
  minetest.chat_send_player(pname, "**You can\'t link a portal to itself!")
  return
  end

  end

  end

  local whitelist = convert_csv_to_table_array(fields.whitelist)

    --Debug Stuff to see and verify if local metadata strings are being stored in table "from_node_metadata" correctly.
    if debug == true then
      minetest.chat_send_player(pname, "<[DEBUG]: "..fields.private..", "..fields.linked..", "..tostring(coordinates)..", "..tostring(whitelist)..">")
      minetest.chat_send_player(pname, "<[DEBUG]: "..tostring(minetest.serialize(portal_list_changes))..">")
      print("<[DEBUG]: "..fields.private..", "..fields.linked..", "..tostring(coordinates)..", "..tostring(whitelist)..">")
      print("<[DEBUG]: "..tostring(minetest.serialize(portal_list_changes))..">")
    end

    storage:set_string("portals_list", minetest.serialize(portal_list_changes))

    --Store data so portal entity can use it to do it's thing
    storage:set_string(fields.portal_name, minetest.serialize({
    ["nametag"] = fields.portal_nametag or "",
    ["portal_name"] = fields.portal_name,
    ["facedir"] = dir,
    ["portal_node_name"] = node,
    ["location"] = portal_pos,
    ["owner"] = owner,
    ["private"] = is_private,
    ["linked"] = is_linked,
    ["linked_portal"] = fields.linked_portal,
    ["coordinates"] = coordinates,
    ["status"] = "Active",
    ["whitelist"] = whitelist, 
    }))

    --Stored Context Data for later use
    previous_input_data = {
    ["nametag"] = fields.portal_nametag or "",
    ["portal_name"] = fields.portal_name,
    ["facedir"] = dir,
    ["portal_node_name"] = node,
    ["owner"] = owner,
    ["portal_pos"] = portal_pos,
    ["private"] = fields.private,
    ["linked"] = fields.linked,
    ["linked_portal"] = fields.linked_portal,
    ["coordinates"] = fields.set_coords, 
    ["whitelist"] = fields.whitelist,
    ["status"] = "Active"
    }

    saved_input_data[fields.portal_name] = {
    ["nametag"] = fields.portal_nametag or "",
    ["portal_name"] = fields.portal_name,
    ["facedir"] = dir,
    ["portal_node_name"] = node,
    ["owner"] = owner,
    ["portal_pos"] = portal_pos,
    ["private"] = fields.private,
    ["linked"] = fields.linked,
    ["linked_portal"] = fields.linked_portal,
    ["coordinates"] = fields.set_coords, 
    ["whitelist"] = fields.whitelist,
    ["status"] = "Active"
    }

    --minetest.chat_send_player(pname, "<[DEBUG] "..tostring(storage:get_string(fields.portal_name)..">"))

  if status == "Inactive" then

  local portal_sounds = storage:get_string(node.."_sounds")
  sound_distance = storage:get_string(node.."_sounds_distance") or 20
  local sound = ""

  --Debug Stuff to see and verify if portal sounds list are being called
  if debug == true then
  minetest.chat_send_player(pname, "<[DEBUG] "..tostring(portal_sounds)..">")
  end

  if portal_sounds then
  portal_sounds = minetest.deserialize(portal_sounds)
  sound = portal_sounds.portal_activate

  --Debug Stuff to see and verify if portal activation sound is being called
  if debug == true then
  minetest.chat_send_player(pname, "<[DEBUG] "..tostring(portal_sounds.portal_activate)..">")
  end

  end

  minetest.sound_play(sound, {
	pos = portal_pos,
	max_hear_distance = sound_distance,
	gain = 10.0,
  })

    

    --The entity that will handle teleportation behavior
    handler_ent = minetest.add_entity({x = portal_pos.x, y = portal_pos.y + storage:get_int(node.."_warp_handler_height"), z = portal_pos.z}, "advanced_portals:portal_handler")
    handler = handler_ent:get_luaentity()
    handler.portal_owner = owner
    handler.scan_radius = storage:get_int(node.."_teleport_radius")
    handler.origin_height = storage:get_int(node.."_warp_handler_height")
    handler.portal = fields.portal_name
    handler.portal_node = tostring("advanced_portals:"..node.."_active")
    handler.portal_nodename = node
    handler.emit_particles = storage:get_string(node.."_activation_particles")


    --The enitiy that acts as the portal's nametag
    nametag_ent = minetest.add_entity({x = portal_pos.x, y = portal_pos.y + storage:get_int(node.."_nametag_height"), z = portal_pos.z}, "advanced_portals:portal_nametag")
    nametag = nametag_ent:get_luaentity()
    nametag.origin_height = storage:get_int(node.."_nametag_height")
    nametag.portal = fields.portal_name
    nametag.portal_node = tostring("advanced_portals:"..node.."_active")
    nametag.portal_nodename = node
    nametag.tag = fields.portal_nametag

  --Swaps node to make it look like if it activated
  minetest.set_node(portal_pos, {name="advanced_portals:"..node.."_active", param2 = dir})

  end

  minetest.chat_send_player(pname, "**Portal \""..fields.portal_name.."\" configuration has been successfully updated!")

  from_node_metadata[pname] = {}

return
end

  portal_list_changes = {}
  previous_portal_ids = {}
  from_node_metadata[pname] = {}

  end
end)

--Clear data
minetest.register_on_leaveplayer(function(player)
  local name = player:get_player_name()
  if from_node_metadata[name] then
  from_node_metadata[name] = nil
  end
end)